<!DOCTYPE html>
<html>
  <head>
    <title>{{ title }}</title>
    <meta name="viewport" content="{{ viewport }}" />
    <link href="{{ favicon_url }}" rel="shortcut icon" />
    <link href="{{ prefix | safe }}/_nicegui/{{version}}/static/nicegui.css" rel="stylesheet" type="text/css" />
    <link href="{{ prefix | safe }}/_nicegui/{{version}}/static/fonts.css" rel="stylesheet" type="text/css" />
    {% if prod_js %}
    <link href="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.prod.css" rel="stylesheet" type="text/css" />
    {% else %}
    <link href="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.css" rel="stylesheet" type="text/css" />
    {% endif %}
    <!-- prevent Prettier from removing this line -->
    {{ head_html | safe }}
  </head>
  <body>
    <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/es-module-shims.js"></script>
    <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/socket.io.min.js"></script>
    {% if tailwind %}
    <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/tailwindcss.min.js"></script>
    {% endif %}
    <!-- prevent Prettier from removing this line -->
    {% if prod_js %}
    <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/vue.global.prod.js"></script>
    <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.umd.prod.js"></script>
    {% else %}
    <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/vue.global.js"></script>
    <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.umd.js"></script>
    {% endif %}

    <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/lang/{{ language }}.umd.prod.js"></script>
    <script type="importmap">
      {"imports": {{ imports | safe }}}
    </script>
    {{ body_html | safe }}

    <div id="app"></div>
    <div id="popup">
      <span>Connection lost.</span>
      <span>Trying to reconnect...</span>
    </div>
    <script>
      function getElement(id) {
        const _id = id instanceof HTMLElement ? id.id : id;
        return window.app.$refs["r" + _id];
      }
      function runMethod(element_id, method_name, args) {
        const element = getElement(element_id);
        if (element === null || element === undefined) return;
        if (method_name in element) {
          return element[method_name](...args);
        } else {
          return element.$refs.qRef[method_name](...args);
        }
      }
      function emitEvent(event_name, ...args) {
        getElement(0).$emit(event_name, ...args);
      }
    </script>
    <script type="module">
      const True = true;
      const False = false;
      const None = undefined;

      const loaded_libraries = new Set();
      const loaded_components = new Set();

      const raw_elements = String.raw`{{ elements | safe }}`;
      const elements = JSON.parse(raw_elements.replace(/&#36;/g, '$')
                                              .replace(/&#96;/g, '`')
                                              .replace(/&gt;/g, '>')
                                              .replace(/&lt;/g, '<')
                                              .replace(/&amp;/g, '&'));

      function stringifyEventArgs(args, event_args) {
        const result = [];
        args.forEach((arg, i) => {
          if (event_args !== null && i >= event_args.length) return;
          let filtered = {};
          if (typeof arg !== 'object' || arg === null || Array.isArray(arg)) {
            filtered = arg;
          }
          else {
            for (let k in arg) {
              if (event_args === null || event_args[i] === null || event_args[i].includes(k)) {
                filtered[k] = arg[k];
              }
            }
          }
          result.push(JSON.stringify(filtered, (k, v) => v instanceof Node || v instanceof Window ? undefined : v));
        });
        return result;
      }

      const waitingCallbacks = new Map();
      function throttle(callback, time, leading, trailing, id) {
        if (time <= 0) {
          // execute callback immediately and return
          callback();
          return;
        }
        if (waitingCallbacks.has(id)) {
          if (trailing) {
            // update trailing callback
            waitingCallbacks.set(id, callback);
          }
        } else {
          if (leading) {
            // execute leading callback and set timeout to block more leading callbacks
            callback();
            waitingCallbacks.set(id, null);
          }
          else if (trailing) {
            // set trailing callback and set timeout to execute it
            waitingCallbacks.set(id, callback);
          }
          if (leading || trailing) {
            // set timeout to remove block and to execute trailing callback
            setTimeout(() => {
              const trailingCallback = waitingCallbacks.get(id);
              if (trailingCallback) trailingCallback();
              waitingCallbacks.delete(id)
            }, 1000 * time);
          }
        }
      }
      function renderRecursively(elements, id) {
        const element = elements[id];
        if (element === undefined) {
            return;
        }

        // @todo: Try avoid this with better handling of initial page load.
        if (element.component) loaded_components.add(element.component.name);
        element.libraries.forEach((library) => loaded_libraries.add(library.name));

        const props = {
          id: 'c' + element.id,
          ref: 'r' + element.id,
          class: element.class.join(' ') || undefined,
          style: Object.entries(element.style).reduce((str, [p, val]) => `${str}${p}:${val};`, '') || undefined,
          ...element.props,
        };
        Object.entries(props).forEach(([key, value]) => {
          if (key.startsWith(':')) {
            try {
              props[key.substring(1)] = eval(value);
              delete props[key];
            }
            catch (e) {
              console.error(`Error while converting ${key} attribute to function:`, e);
            }
          }
        });
        element.events.forEach((event) => {
          let event_name = 'on' + event.type[0].toLocaleUpperCase() + event.type.substring(1);
          event.specials.forEach(s => event_name += s[0].toLocaleUpperCase() + s.substring(1));
          let handler = (...args) => {
            const data = {
              id: element.id,
              client_id: window.client_id,
              listener_id: event.listener_id,
              args: stringifyEventArgs(args, event.args),
            };
            const emitter = () => window.socket?.emit("event", data);
            throttle(emitter, event.throttle, event.leading_events, event.trailing_events, event.listener_id);
            if (element.props["loopback"] === False && event.type == "update:modelValue") {
              element.props["model-value"] = args;
            }
          };
          handler = Vue.withModifiers(handler, event.modifiers);
          handler = event.keys.length ? Vue.withKeys(handler, event.keys) : handler;
          if (props[event_name]) {
            props[event_name].push(handler)
          } else {
            props[event_name] = [handler];
          }
        });
        const slots = {};
        Object.entries(element.slots).forEach(([name, data]) => {
          slots[name] = (props) => {
            const rendered = [];
            if (data.template) {
              rendered.push(Vue.h({
                props: { props: { type: Object, default: {} } },
                template: data.template,
              }, {
                props: props,
              }));
            }
            const children = data.ids.map(id => renderRecursively(elements, id));
            if (name === 'default' && element.text !== null) {
              children.unshift(element.text);
            }
            return [ ...rendered, ...children]
          }
        });
        return Vue.h(Vue.resolveComponent(element.tag), props, slots);
      }

      function runJavascript(code, request_id) {
        (new Promise((resolve) =>resolve(eval(code)))).catch((reason) => {
          if(reason instanceof SyntaxError)
            return eval(`(async() => {${code}})()`);
          else
            throw reason;
        }).then((result) => {
          if (request_id) {
            window.socket.emit("javascript_response", {request_id, client_id: window.client_id, result});
          }
        });
      }

      function download(src, filename) {
        const anchor = document.createElement("a");
        anchor.href = typeof src === "string" ? src : URL.createObjectURL(new Blob([src]));
        anchor.target = "_blank";
        anchor.download = filename || "";
        document.body.appendChild(anchor);
        anchor.click();
        document.body.removeChild(anchor);
        if (typeof src !== "string") {
          URL.revokeObjectURL(url);
        }
      }

      async function loadDependencies(element) {
        if (element.component) {
          const {name, key, tag} = element.component;
          if (!loaded_components.has(name) && !key.endsWith('.vue')) {
            const component = await import(`{{ prefix | safe }}/_nicegui/{{version}}/components/${key}`);
            app = app.component(tag, component.default);
            loaded_components.add(name);
          }
        }
        for (const {name, key} of element.libraries) {
          if (loaded_libraries.has(name)) continue;
          await import(`{{ prefix | safe }}/_nicegui/{{version}}/libraries/${key}`);
          loaded_libraries.add(name);
        }
      }

      let app = Vue.createApp({
        data() {
          return {
            elements,
          };
        },
        render() {
          return renderRecursively(this.elements, 0);
        },
        mounted() {
          window.app = this;
          const query = {{ socket_io_js_query_params | safe }};
          window.client_id = query.client_id;
          const url = window.location.protocol === 'https:' ? 'wss://' : 'ws://' + window.location.host;
          const extraHeaders = {{ socket_io_js_extra_headers | safe }};
          const transports = {{ socket_io_js_transports | safe }};
          window.path_prefix = "{{ prefix | safe }}";
          window.socket = io(url, { path: "{{ prefix | safe }}/_nicegui_ws/socket.io", query, extraHeaders, transports });
          const messageHandlers = {
            connect: () => {
              window.socket.emit("handshake", window.client_id, (ok) => {
                if (!ok) {
                  console.log('reloading because handshake failed for client_id ' + window.client_id)
                  window.location.reload();
                }
                document.getElementById('popup').style.opacity = 0;
              });
            },
            connect_error: (err) => {
              if (err.message == 'timeout') {
                console.log('reloading because connection timed out')
                window.location.reload(); // see https://github.com/zauberzeug/nicegui/issues/198
              }
            },
            try_reconnect: async () => {
              document.getElementById('popup').style.opacity = 1;
              await fetch(window.location.href, { headers: { 'NiceGUI-Check': 'try_reconnect' } });
              console.log('reloading because reconnect was requested')
              window.location.reload();
            },
            disconnect: () => {
              document.getElementById('popup').style.opacity = 1;
            },
            update: async (msg) => {
              for (const [id, element] of Object.entries(msg)) {
                if (element === null) {
                  delete this.elements[id];
                  continue;
                }
                if (element.component || element.libraries.length > 0) {
                  await loadDependencies(element);
                }
                this.elements[element.id] = element;
              }
            },
            run_javascript: (msg) => runJavascript(msg['code'], msg['request_id']),
            open: (msg) => {
              const url = msg.path.startsWith('/') ? "{{ prefix | safe }}" + msg.path : msg.path;
              const target = msg.new_tab ? '_blank' : '_self';
              window.open(url, target);
            },
            download: (msg) => download(msg.src, msg.filename),
            notify: (msg) => Quasar.Notify.create(msg),
          };
          const socketMessageQueue = [];
          let isProcessingSocketMessage = false;
          for (const [event, handler] of Object.entries(messageHandlers)) {
            window.socket.on(event, async (...args) => {
              socketMessageQueue.push(() => handler(...args));
              if (!isProcessingSocketMessage) {
                while (socketMessageQueue.length > 0) {
                  const handler = socketMessageQueue.shift()
                  isProcessingSocketMessage = true;
                  try {
                    await handler();
                  }
                  catch (e) {
                    console.error(e);
                  }
                  isProcessingSocketMessage = false;
                }
              }
            });
          }
        },
      }).use(Quasar, {
        config: {{ quasar_config | safe }}
      });

      {{ js_imports | safe }}
      {{ vue_scripts | safe }}

      const dark = {{ dark }};
      Quasar.lang.set(Quasar.lang["{{ language }}".replace('-', '')]);
      Quasar.Dark.set(dark === None ? "auto" : dark);
      {% if tailwind %}
      if (dark !== None) tailwind.config.darkMode = "class";
      if (dark === True) document.body.classList.add("dark");
      {% endif %}

      app.mount("#app");
    </script>
  </body>
</html>
